/* 
 * Copyright (c) 2012 Christoph Mueller <ruunhb@googlemail.com>
 * 
 * Crystal is free software: you can redistribute it and/or modify
 * it under the terms of the Lesser GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Crystal is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * Lesser GNU General Public License for more details.
 *
 * You should have received a copy of the Lesser GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "unittest.h"
#include "base.h"
#include "unix_colors.h"
#include <sys/signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <getopt.h>
#include <fnmatch.h>

#define COLOR(condition, color, string) (condition ? color string CRY_NORMAL : string)

// Module internal helper functions
static int  monitor_testprocess(pid_t worker_pid, unsigned int timeout);
static void update_statistics(int return_code);
static void release_module_ressource(int signal);
static void lq_unittest_run_forked(const char* desc, lq_modulechecker handler, const_pointer data, unsigned int timeout);
static void print_usage(FILE* stream, const char* programm, int exit_code);
static void print_verbose_testprocess_status(const char* desc, int return_code, suseconds_t time);
static void print_testprocess_status(int return_code);

// Current Errorfile
static char* Testlog = 0;

// Options
static int Colors = FALSE;
static int Repeat = TRUE;
static int Verbose = FALSE;
static int Allow = FALSE;
static int Fork = FALSE;
static const char* FilterPattern = 0;

// Statistics
static int TestCaseNumber = 0;
static int SegfaultNumber = 0;
static int TimeoutNumber = 0;
static int FailureNumber = 0;
static int SkippedNumber = 0;


static void
release_module_ressource(int signal)
{
        if(Testlog != 0) {
                free(Testlog);                
        }
}

void
lq_unittest_initialize(int argc, char** argv)
{
	int ch = 0;
        time_t current_time = time(0);
        struct tm* time_struct = localtime(&current_time);
	
	static struct option options[] = {
		{"help", no_argument, 0, 'h'},
		{"colors", no_argument, 0, 'c'},
		{"verbose", no_argument, 0, 'v'},
		{"repeat", optional_argument, 0, 'r'},
		{"filter", optional_argument, 0, 'f'},
		{"fork", no_argument, 0, 'k'},
		{"allow", no_argument, 0, 'a'}
	};

	while( (ch = getopt_long(argc, argv, "vhakcr:f:", options, 0)) != -1 ) {
		switch(ch) {
			case 'h':
				print_usage(stdout, argv[0], EXIT_SUCCESS);
			case 'c':
				Colors = 1;
				break;
			case 'r':
				Repeat = atoi(optarg);
				break;
			case 'f':
				FilterPattern = optarg;
				break;
			case 'a':
				Allow = 1;
				break;
			case 'k':
				Fork = 1;
				break;
			case 'v':
				Verbose = 1;
				break;
			case '?':
				print_usage(stderr, argv[0], EXIT_FAILURE);
			default:
				abort();
		}
	}

	if(Repeat == 0) {
		fprintf(stderr, "A parsing error occured for the repeat argument or is set to zero.\n");
		print_usage(stderr, argv[0], EXIT_FAILURE);
	}

	printf(":: colors %s, fork %s, repeat %d, filter %s ::\n\n", 
                        Colors ? "ON" : "OFF", 
                        Fork ? "ON" : "OFF",
                        Repeat, 
                        FilterPattern ? FilterPattern : "OFF");

        if(Testlog == 0) {
                Testlog = calloc(sizeof(char), 64);

                strftime(Testlog, 64, "%Y%m%d_%H%M%S.testlog", time_struct);
        }

        signal(SIGTERM, release_module_ressource);
        signal(SIGINT, release_module_ressource);
}



void lq_unittest_finalize(void)
{
	int Failures = SegfaultNumber + TimeoutNumber + FailureNumber;
	FILE* log = fopen(Testlog, "r");
	char buffer[128];

        if(!Verbose)
                printf("\n\n");
	else
		printf("\n");

	printf("%d tests, %d skipped, %d failures, %d segfaults, %d timeouts\n\n", TestCaseNumber, SkippedNumber, Failures, SegfaultNumber, TimeoutNumber);

	while(log != 0 && !feof(log)) {
		if(fgets(buffer, 128, log) != 0)
			printf(COLOR(Colors, CRY_RED, "%s"), buffer);
	}

	if(log != 0)
		fclose(log);

	printf("\n");

        free(Testlog);
}



void 
lq_unittest_run(const char* desc, lq_modulechecker handler, const_pointer data, unsigned int timeout)
{
	if(FilterPattern && fnmatch(FilterPattern, desc, 0) == FNM_NOMATCH) {
		SkippedNumber++;
		return;
	}

	if(Fork) {
		lq_unittest_run_forked(desc, handler, data, timeout);
		return;
	}
        
	struct timeval start, end;
	int i = 0;

	//if(!Allow)
	//	freopen("/dev/null", "a", stdout);

	gettimeofday(&start, 0);

	for(i = 0; i < Repeat; ++i)
		handler(data);

	gettimeofday(&end, 0);

	//if(!Allow)
	//	fclose(stdout);

	time_t diff_s = end.tv_sec - start.tv_sec;
	suseconds_t diff_ms = end.tv_usec - start.tv_usec;

	update_statistics(EXIT_SUCCESS);

	if(Verbose) {
		print_verbose_testprocess_status(desc, EXIT_SUCCESS, diff_s * 1000000L + diff_ms);
	} else {
		print_testprocess_status(EXIT_SUCCESS);
	}

	fflush(stdout);

}


static void
lq_unittest_run_forked(const char* desc, lq_modulechecker handler, const_pointer data, unsigned int timeout)
{
	pid_t process_pid = fork();
        
	struct timeval start, end;

	if(process_pid > 0) {
	 	gettimeofday(&start, 0);

		// Spawning monitoring and timeout process for test case
		int status = monitor_testprocess(process_pid, timeout);

		gettimeofday(&end, 0);

		time_t diff_s = end.tv_sec - start.tv_sec;
		suseconds_t diff_ms = end.tv_usec - start.tv_usec;

		update_statistics(status);

                if(Verbose) {
        		print_verbose_testprocess_status(desc, status, diff_s * 1000000L + diff_ms);
                } else {
                        print_testprocess_status(status);
                }

                fflush(stdout);
                
	} else {
		int i = 0;

		// Process for running test case
                freopen(Testlog, "a", stderr);

		if(!Allow)
                	freopen("/dev/null", "a", stdout);

		for(i = 0; i < Repeat; ++i)
			handler(data);

		fclose(stderr);

		if(!Allow) {
                	fclose(stdout);
                }

                free(Testlog);

		exit(EXIT_SUCCESS);
	}
}

	
	
static void 
print_usage(FILE* stream, const char* programm, int exit_code)
{
	fprintf(stream, "Usage: %s options\n", programm);
	fprintf(stream, 
		"   -h   --help            Display this usage information for your testcase.\n"
		"   -c   --colors          Enable color support for test output.\n"
                "   -v   --verbose         Show more details about the testing progress.\n"
		"   -r   --repeat=INT      Set number of repeats for each testcase.\n"
		"   -a   --allow           Allow stdout output for test files.\n"
		"   -k   --fork            Run tests in its own process.\n"
		"   -f   --filter=PATTERN  Set a regular expression filter for all testcases.\n");
	exit(exit_code);
}



static int
monitor_testprocess(pid_t worker_pid, unsigned int timeout)
{
	pid_t timeout_pid = fork();

	if(timeout_pid > 0) {
		int status = 0;
		pid_t pid = waitpid(worker_pid, &status, 0);

		if(pid == worker_pid) {
			kill(timeout_pid, SIGTERM);
			return status;
	        } else if(pid == EINTR) {
			return SIGINT;
		} else
			return SIGILL;
	} else {
		sleep(timeout);
		kill(worker_pid, SIGINT);

                exit(EXIT_SUCCESS);
	}
}


static void
update_statistics(int return_code)
{
	if(return_code == SIGINT)
		TimeoutNumber++;
	else if(WCOREDUMP(return_code) || return_code == SIGSEGV)
		SegfaultNumber++;
	else if(!WIFEXITED(return_code))
		FailureNumber++;
	
	TestCaseNumber++;
}


static void 
print_verbose_testprocess_status(const char* desc, int return_code, suseconds_t time)
{
	char buffer[128];

	snprintf(buffer, 128, "%s (%.3lf ms)", desc, time / 1000.0);

	printf("%-70s ", buffer);

	if(return_code == SIGINT) {
		printf(COLOR(Colors, CRY_RED, "TIMEOUT"));
        } else if(WIFEXITED(return_code)) {
		printf(COLOR(Colors, CRY_GREEN, "OK"));                
        } else if(WCOREDUMP(return_code) || return_code == SIGSEGV) {
		printf(COLOR(Colors, CRY_RED, "SEGFAULT"));
        } else {
		printf(COLOR(Colors, CRY_RED, "FAIL (%d)"), return_code);
        }

	printf("\n");
}


static void
print_testprocess_status(int return_code)
{
        if(return_code == SIGINT) {
                printf(COLOR(Colors, CRY_RED, "T"));
        } else if(WIFEXITED(return_code)) {
                printf(COLOR(Colors, CRY_GREEN, "."));
        } else if(WCOREDUMP(return_code) || return_code == SIGSEGV) {
                printf(COLOR(Colors, CRY_RED, "S"));
        } else {
                printf(COLOR(Colors, CRY_RED, "F"));
        }
}

